fix(core): refuse effect/listener/plugin registration while a fiber is UNLOADING - #96
Open
inso1337 wants to merge 1 commit into
Open
fix(core): refuse effect/listener/plugin registration while a fiber is UNLOADING#96inso1337 wants to merge 1 commit into
inso1337 wants to merge 1 commit into
Conversation
…s UNLOADING
Fiber.assertActive() only checked uid !== null ('not disposed'), not
'not unloading'. During a deactivation (a requirement withdrawn, the fiber
survives as PENDING), an undo that calls ctx.effect() was accepted and the
resulting disposer leaked permanently: the new effect's disposer was pushed
after the unload snapshot was already taken, and no later dispose() ever ran
it — permanent residue (the G5 gap).
The UNLOADING check is NOT added to assertActive() itself, because update()
and restart() legitimately run through an inertial reload's UNLOADING pass.
Instead the guard is scoped to registration entry points:
- effect() -> assertRegistrable()
- events on() -> assertRegistrable()
- registry plugin() -> assertRegistrable()
assertRegistrable() throws INACTIVE_EFFECT when the fiber is disposed OR
unloading; update()/restart() keep the disposed-only check.
Regression test (fiber.spec.ts 'refuses effect registration while UNLOADING'):
a provider dispose withdraws 'svc', the consumer's generator-effect undo
calls ctx.effect() during teardown — leaks=false, effects=0, PENDING. Fails
before the fix (leaked=true), passes after. Full core suite: 71 passed.
inso1337
pushed a commit
to inso1337/cordis
that referenced
this pull request
Aug 24, 2026
…restart() The broad assertActive() guard broke update() during an inertial reload (which legitimately passes through UNLOADING) — the fork's existing 'update config while injected service reloads' test caught it. Move the UNLOADING check to assertRegistrable(), used by effect()/on()/plugin(); update()/restart() keep the disposed-only check. Matches upstream PR (cordiverse/cordis#96).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fiber.assertActive()only checkeduid !== null— "not disposed", not"not unloading". During a deactivation (a requirement is withdrawn and the
fiber survives as
PENDING), an undo that callsctx.effect(...)wasaccepted, and the resulting disposer leaked permanently:
fiber._disposables— after_unloadalreadyclear()ed the list;PENDINGwhile still holding an effect(
fiber.getEffects().length > 0on an inactive fiber);fiber.dispose()never ran that disposer, because the epochis already
INACTIVEand no further unload is triggered — permanentresidue.
Fix
The
UNLOADINGlifecycle check is added to a newassertRegistrable()usedby the registration entry points —
effect(),on(),plugin()— andNOT to
assertActive()itself, becauseupdate()andrestart()must keepworking through an inertial reload's
UNLOADINGpass (a broad guard brokethe existing "update config while injected service reloads" test).
Repro / regression test
packages/core/tests/fiber.spec.ts— "refuses effect registration whileUNLOADING (G5 residue)": a provider dispose withdraws
svc, the consumer'sgenerator-effect undo calls
ctx.effect()during teardown. Before the fixleaked === trueand the fiber holds 1 leaked effect; after,leaked === false,getEffects().length === 0, statePENDING.Full core suite: 71 passed.
Notes
revl's TS tier pins this exact behavior. Related PR feat(core): make effect disposal and Fiber lifecycle reentrant-safe #39 (reentrant fiber
lifecycle) guards
effect()directly; this change places the guard on theshared registration path so
on()/plugin()inherit it. The two can bereconciled during feat(core): make effect disposal and Fiber lifecycle reentrant-safe #39's review.
FAILEDfibers still pass (as before):update()on a failed fiber is alegitimate recovery path.